/**
 * 
 */
package jmine.tec.utils.cripto.cipher;

import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.security.InvalidKeyException;
import java.security.Key;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;

/**
 * Implementa um {@link InputStream} para decriptar um {@link InputStream} encriptado em 'chunks'. O formato esperado é um int com o tamanho
 * do proximo chunk e um array de bytes (puro) com os dados a serem decriptados.
 * <p>
 * Internamente, esta classe utiliza um buffer com 1024 bytes.
 * 
 * @author takeshi
 * @version 20/08/2007
 */
public final class CipherInputStream extends InputStream {

    /**
     * 
     */
    private static final int DEFAULT_BUFFER_SIZE = 1024;

    private final Cipher cipher;

    private final DataInputStream input;

    private byte[] buffer;

    /**
     * current read position from the buffer
     */
    private int position = 0;

    /**
     * index of last read and decoded byte
     */
    private int read = 0;

    private boolean done = false;

    /**
     * C'tor.
     * <p>
     * O {@link Cipher} passado não deve ser compartilhado.
     * 
     * @param input the original input
     * @param cipher the {@link Cipher} used to decode
     * @param key the key to decrypt
     * @throws InvalidKeyException if the key is invalid
     */
    public CipherInputStream(final InputStream input, final Cipher cipher, final Key key) throws InvalidKeyException {
        super();
        this.input = new DataInputStream(input);
        this.cipher = cipher;
        this.buffer = new byte[DEFAULT_BUFFER_SIZE];
        this.cipher.init(Cipher.DECRYPT_MODE, key);
    }

    /**
     * @return int or -1
     * @throws IOException if an {@link IOException} occurs while reading from the source {@link InputStream}
     * @see java.io.InputStream#read()
     */
    @Override
    public synchronized int read() throws IOException {
        if (this.done) {
            return -1;
        }
        if (this.position < this.read) {
            return this.buffer[this.position++];
        }
        this.decodeNextChunk();
        return this.read();
    }

    /**
     * Le e decripta em um array de bytes. Este método é preferido sobre a implementaçao padrão por questões de performance.
     * <p>
     * Classes clientes desse método DEVEM se preocupar com o valor de retorno do metodo.
     * 
     * @param b o array de bytes onde os dados serão escritos
     * @param off o indice de onde comecar
     * @param len quantidade de dados a ser lido
     * @return int quantidade de dados efetivamente lida ou -1 se não há dados disponíveis.
     * @throws IOException se um erro de IO acontecer
     * @see java.io.InputStream#read(byte[], int, int)
     */
    @Override
    public synchronized int read(final byte[] b, final int off, final int len) throws IOException {
        if (this.done) {
            return -1;
        }
        if (len == 0 || off < 0) {
            return 0;
        }
        if (this.position < this.read) {
            int toRead = Math.min(len, this.read - this.position);
            System.arraycopy(this.buffer, this.position, b, off, toRead);
            this.position += toRead;
            return toRead;
        }
        this.decodeNextChunk();
        return this.read(b, off, len);
    }

    /**
     * Reads i bytes
     * 
     * @throws IOException
     * @throws IOException if an {@link IOException} is thrown
     */
    private void decodeNextChunk() throws IOException {
        if (this.done) {
            return;
        }
        try {
            int toRead;
            toRead = this.readInt();
            if (toRead < 0) {
                this.done = true;
                return;
            }
            byte[] buf = new byte[toRead];
            this.input.readFully(buf);
            byte[] decoded;
            decoded = this.cipher.doFinal(buf);
            this.read = decoded.length;
            if (this.read > this.buffer.length) {
                this.buffer = new byte[this.read];
            }
            System.arraycopy(decoded, 0, this.buffer, 0, this.read);
            this.position = 0;
        } catch (IllegalBlockSizeException e) {
            throw new IllegalStateException(e);
        } catch (BadPaddingException e) {
            throw new IllegalStateException(e);
        }
    }

    /**
     * Reads an integer from the input. It does not delegate to the input's readInt method so i can check for end of stream at the first
     * byte and gracefully set done flag and return. Otherwise, throw an {@link EOFException}.
     * 
     * @return int
     * @throws IOException if the stream ended unexpectedly or if the stream throws an exception
     */
    private int readInt() throws IOException {
        int toRead;
        int ch1 = this.input.read();
        if (ch1 < 0) {
            return -1;
        }
        int ch2 = this.input.read();
        int ch3 = this.input.read();
        int ch4 = this.input.read();
        if ((ch2 | ch3 | ch4) < 0) {
            throw new EOFException("Unexpected end of file reached!");
        }
        final int firstPadding = 24;
        final int secondPadding = 16;
        final int thirdPadding = 8;
        toRead = ((ch1 << firstPadding) | (ch2 << secondPadding) | (ch3 << thirdPadding) | (ch4 << 0));
        return toRead;
    }

    /**
     * @throws IOException e
     * @see java.io.InputStream#close()
     */
    @Override
    public void close() throws IOException {
        this.input.close();
    }

}
